package br.com.ime.usp.cmep;

import java.io.UnsupportedEncodingException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

public class BandexUtils {
	
	// nao da pra instanciar!
	private BandexUtils() { }
	
	// ----------------------------------------------------------------------------
	// pega lista de bandexs
	// ----------------------------------------------------------------------------
	public static String[] getList() {
		return new String[] { "Central", "Química", "Física", "COCESP" };
	}
	
	// ----------------------------------------------------------------------------
	// um comentario de bandex
	// ----------------------------------------------------------------------------
	public static class BandexComment {
		private String texto, hora, fila, bandex;
		
		public BandexComment(String pbandex, String ptexto, String pfila, String phora) throws Exception {
			this.texto = ptexto;
			this.hora = phora;
			this.fila = filaKeyword(pfila);
			this.bandex = pbandex;
		}
		
		public BandexComment(String pbandex, String ptexto, String pfila) throws Exception {
			this.texto = ptexto;
			this.hora = "";
			this.fila = filaKeyword(pfila);
			this.bandex = pbandex;
		}
		
		public String getTexto() { return texto; }
		public String getHora() { return hora; }
		public String getFila() { return fila; }
		public String getBandex() { return bandex; }
		
		public boolean equals(BandexComment c) {
			return (bandex + texto + fila).equals(c.bandex + c.texto + c.fila);
		}
		
		// ---------
		
		private String filaKeyword(String fila) throws Exception {
			if(fila.equals("Sem fila") || fila.equals("SEM_FILA"))
				return "SEM_FILA";
			else if(fila.equals("Pequena") || fila.equals("PEQUENA"))
				return "PEQUENA";
			else if(fila.equals("Fila média") || fila.equals("Média") || fila.equals("MEDIA"))
				return "MEDIA";
			else if(fila.equals("Grande") || fila.equals("GRANDE"))
				return "GRANDE";
			else if(fila.equals("Enorme") || fila.equals("Muito grande") || fila.equals("MUITO_GRANDE"))
				return "MUITO_GRANDE";

			throw new Exception("Tamanho de fila invalido: " + fila);
		}
	}
	
	// ----------------------------------------------------------------------------
	// estado geral de um bandex (nome, tamanho da fila, etc.)
	// ----------------------------------------------------------------------------
	public static class BandexStatus {
		private String bandex; // NOME do bandex
		private double fila; // media amostral da fila. 0 (vazia) <= fila <= 1 (cheia)
		private double desvio; // desvio padrao
		private int samples; // numero de amostras
		private double erro; // margem de erro (erro amostral)
		private double conf; // confianca c, relacionado ao intervalo de confianca a 100 x (1-c)% para a media
		
		// getters ;)
		public String getBandex() { return bandex; }
		public double getTamanhoDaFila() { return fila; } // media amostral
		public int getNumeroDeAmostras() { return samples; } 
		public double getMargemDeErro() { return erro; }
		public double getConfianca() { return conf; } // constante
		public double getDesvioPadrao() { return desvio; } // na vdd, e' a raiz de um ESTIMADOR da variancia
		
		// construtor padrao
		public BandexStatus(String pbandex) throws Exception {
			this(pbandex, new BandexNetworkAccessor(), 0.95);
		}
		
		// mais um construtor
		public BandexStatus(String pbandex, BandexAccessor ba) throws Exception {
			this(pbandex, ba, 0.95);
		}
		
		// recebe os comentarios de um bandex e cria o status
		public BandexStatus(String pbandex, BandexAccessor ba, double confianca) throws Exception {
			bandex = pbandex;
			conf = confianca;
			
			ArrayList<BandexComment> comments = ba.getBandexComments(bandex);
			if((samples = comments.size()) > 0) {
				double avg = 0.0, var = 0.0, d;
				
				// media amostral
				for(BandexComment c : comments)
					avg += quantify(c.getFila());
				avg /= (double)samples;
				
				// estimador da variancia
				for(BandexComment c : comments) {
					d = quantify(c.getFila()) - avg;
					var += d * d;
				}
				var /= (double)Math.max(1, samples - 1);
				
				// o tamanho da fila sera' dado pela media amostral
				fila = avg;
				
				// o desvio padrao...
				desvio = Math.sqrt(var);
				
				// nossa margem de erro sera' ...
				// lembrar do Teorema do Limite Central
				erro = Gaussian.PhiInverse( 1.0 - (1.0 - confianca) / 2.0 ) * Math.sqrt(var / (double)samples);
				
				// intervalo de 95% de confianca para a media amostral:
				// [ avg - erro, avg + erro ]
			}
			else {
				fila = 0.5;
				erro = 0.5;
				desvio = 0.0;
			}
		}
		
		// uma formuleta q inventamos. qto + proximo de 1, melhor eh a medida. qto + proximo de zero, pior.
		// a formula e' baseada no numero de amostras e na margem de erro, feita por meio de inferencia estatistica.
		public double getPrecisao() {
			return Math.max(Math.min(1.0 - 2.0 * erro, 1.0), 0.0) * Math.min(1.0, (double)samples / 30.0); 
		}
		
		// --------------------
		
		// transforma um tamanho de fila num numero de 0 a 1
		private double quantify(String fila) {
			if(fila.equals("SEM_FILA"))
				return 0.0;
			else if(fila.equals("PEQUENA"))
				return 0.25;
			else if(fila.equals("MEDIA"))
				return 0.5;
			else if(fila.equals("GRANDE"))
				return 0.75;
			else if(fila.equals("MUITO_GRANDE"))
				return 1.0;
			else
				return 0.5;
		}
	}
	
	// ----------------------------------------------------------------------------
	// interface de acesso dos dados dos Bandexs
	// ----------------------------------------------------------------------------
	public interface BandexAccessor {
		// comentarios
		public ArrayList<BandexComment> getBandexComments(String bandex) throws Exception; // pega comentarios de um certo bandex
		public void postBandexComment(BandexComment c) throws Exception; // envia dados de um bandex
		
		// cardapio
		public String getCardapio(String bandex) throws Exception; // pega o cardapio de hoje
		public String getCardapio(String bandex, int weekday) throws Exception; // pega o cardapio de 0 (dom) <= weekday < 7
	}
	
	// ----------------------------------------------------------------------------
	// mock accessor, para testes. Tudo é local.
	// ----------------------------------------------------------------------------
	public static class BandexMockAccessor implements BandexAccessor {
		private HashMap<String, ArrayList<BandexComment> > db;
		
		public BandexMockAccessor() {
			db = new HashMap<String, ArrayList<BandexComment> >();
		}
		
		public ArrayList<BandexComment> getBandexComments(String bandex) throws Exception {
			bandex = bandex.toLowerCase();
			if(null == db.get(bandex))
				db.put(bandex, new ArrayList<BandexComment>());
			return db.get(bandex);
		}
		
		public void postBandexComment(BandexComment c) throws Exception {
			getBandexComments(c.getBandex()).add(c);
		}

		public String getCardapio(String bandex) throws Exception {
			return getCardapio(bandex, 1);
		}
		
		public String getCardapio(String bandex, int weekday) throws Exception {
			if(!Arrays.asList(getList()).contains(bandex))
				throw new Exception("Bandex inválido: " + bandex);
			return "Fechado";
		}
	}
	
	// ----------------------------------------------------------------------------
	// acessa os dados dos Bandexs pela rede
	// ----------------------------------------------------------------------------
	public static class BandexNetworkAccessor implements BandexAccessor {
		public ArrayList<BandexComment> getBandexComments(String bandex) throws Exception {
			String json = HttpUtils.get(getCommentURL(bandex));
			if(!isValidJSON(json))
				throw new Exception("Não pude verificar o estado dos bandexs!");
			return JSON2ArrayList(json, bandex);
		}
	
		public void postBandexComment(BandexComment c) throws Exception {
			HashMap<String,String> params = new HashMap<String,String>();
			params.put("comentario.texto", c.getTexto());
			params.put("comentario.fila", c.getFila());
			if(null == HttpUtils.post(getCommentURL(c.getBandex()), params))
				throw new Exception("Erro ao enviar comentário");
		}
		
		public String getCardapio(String bandex) throws Exception {
			return getCardapio(bandex, Calendar.getInstance().get(Calendar.DAY_OF_WEEK));
		}
		
		public String getCardapio(String bandex, int weekday) throws Exception {
			String almoco = "", janta = "";
			String wd = weekdayString(weekday);
			String html = HttpUtils.get(getCardapioURL(bandex), "ISO-8859-1");
			
			if(html == null)
				throw new Exception("Impossível obter cardápio.");
			
			String expr = "<tr>.*?<td.*?" + wd + "(.*?)<\\/td>.*?<td.*?" + wd + "(.*?)<\\/td>";
			Pattern patt = Pattern.compile(expr, Pattern.DOTALL | Pattern.UNIX_LINES | Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
			Matcher m = patt.matcher(html);
			while(m.find()) {
				almoco += m.group(1);
				janta += m.group(2);
			}
			almoco = almoco.trim();
			janta = janta.trim();
			
			if(almoco.equals("") && janta.equals("")) {
				expr = "<tr>.*?<td.*?" + wd + "(.*?)<\\/td>";
				patt = Pattern.compile(expr, Pattern.DOTALL | Pattern.UNIX_LINES | Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
				m = patt.matcher(html);
				while(m.find())
					almoco += m.group(1);
				almoco = almoco.trim();
			}
			
			if((almoco = filterOutHtmlTags(almoco)).equals(""))
				almoco = "Fechado";
			
			if((janta = filterOutHtmlTags(janta)).equals(""))
				janta = "Fechado";
			
			return "ALMOÇO\n\n" + almoco + "\n\nJANTAR\n\n" + janta;
		}
		
		// ------------------------
		
		// pega url do servidor remoto de comentarios
		private String getCommentURL(String bandex) {
			String b = StringSanitizer.convertNonAscii(bandex).toLowerCase(); // b é "central", "fisica", "quimica" ou "cocesp"
			return "http://valinhos.ime.usp.br:56080/usp-mobile/bandejao/" + b;
		}
		
		// pega url do servidor remoto de cardapio
		private String getCardapioURL(String bandex) throws Exception {
			String b = StringSanitizer.convertNonAscii(bandex).toLowerCase();
			if(b.equals("central"))
				return "http://www.usp.br/coseas/cardapio.html";
			else if(b.equals("fisica"))
				return "http://www.usp.br/coseas/cardapiofisica.html";
			else if(b.equals("quimica"))
				return "http://www.usp.br/coseas/cardapioquimica.html";
			else if(b.equals("cocesp"))
				return "http://www.usp.br/coseas/cardcocesp.html";
			else
				throw new Exception("Bandex inválido: " + bandex);
		}
		
		// nomezinho especial para o weekday
		private String weekdayString(int weekday) {
			switch(weekday) {
			case 2: return "SEGUNDA.FEIRA";
			case 3: return "TER.A.FEIRA";
			case 4: return "QUARTA.FEIRA";
			case 5: return "QUINTA.FEIRA";
			case 6: return "SEXTA.FEIRA";
			case 7: return "S.BADO"; // encoding
			default: return "DOMINGO";
			}
		}
		
		// temos um json valido?
		private boolean isValidJSON(String data) {
			try {
				return null != new JSONObject(data);
			}
			catch(Exception e) {
				return false;
			}
		}
		
		// le json, joga numa lista de hash
		private ArrayList<BandexComment> JSON2ArrayList(String remoteJSON, String bandex) {
		    ArrayList<BandexComment> lst = new ArrayList<BandexComment>();
		    
		    // cria lista de hash
		    try {
		    	JSONObject remoteData = new JSONObject(remoteJSON);
		    	JSONArray commentList = remoteData.getJSONArray("list");
		    	for (int i=0; i<commentList.length(); i++) {
					JSONObject comment = commentList.getJSONObject(i);
					lst.add(new BandexComment(
							bandex,
							comment.has("texto") ? comment.getString("texto") : "-",
							comment.has("fila") ? comment.getString("fila") : "MEDIA",
							comment.has("hora") ? comment.getString("hora") : "00-00-00-01-01-2001"
					));
				}
		    }
		    catch(Exception e) {
		    	;
		    }
		    
		    // pronto!!! ;-)
		    return lst;
		}
		
		// tira as tags html de um texto
		private String filterOutHtmlTags(String text) {
			return text
					.replaceAll("^\n", "")
					.replaceAll("<FONT", "<font")
					.replaceAll("</FONT", "</font")
					.replaceAll("<\\/font>.*?<font", "\n<font")
					.replaceAll("<.*?>", "")
					.replaceAll("&.*?;", "")
					.replaceAll("[ \t]{2,}", " ")
					.replaceAll("\n[ \t]*", "\n")
					.trim();
		}
	}
}
